Skip to content

Additional outputs to longitudinal functional workflow Fix #190#282

Closed
jpillai00 wants to merge 6 commits into
mainfrom
add/long-func-outputs
Closed

Additional outputs to longitudinal functional workflow Fix #190#282
jpillai00 wants to merge 6 commits into
mainfrom
add/long-func-outputs

Conversation

@jpillai00
Copy link
Copy Markdown
Contributor

The longitudinal functional workflow previously only warped cross-sectional outputs (sbref, preproc BOLD, brain mask, and the BOLD-to-longitudinal transform) into longitudinal template space. This PR extends it to recompute nuisance regression in longitudinal space using the raw (non-bandpass-filtered) regressors from cross-sectional preprocessing.

Key changes:

  • FunctionalLongOutputs now includes regressed_bold and cleaned_bold as dict[str, Path], supporting multiple regressor strategies
  • longitudinal_process accepts regressor_files: dict[str, Path] and re-runs both non-bandpassed and bandpassed regression in longitudinal space
  • FunctionalOutputs separately exports regressor_file (raw, passed to longitudinal workflow) and bpf_regressor_file (bandpass-filtered) - in cross sectional
  • resolve_longitudinal_func resolves raw regressor files per regressor strategy
  • export_longitudinal_func saves per-regressor regressed_bold and cleaned_bold
  • --regressor argument added to the longitudinal CLI

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 8, 2026

Coverage

Coverage Report
FileStmtsMissCoverMissing
rbc
   __init__.py10100% 
   context.py25868%70, 75–77, 79–80, 93–94
   metadata.py560100% 
rbc/bids
   __init__.py90100% 
   _schema.py585499%776, 782, 790, 1101
   anatomical.py24387%44, 47–48
   builder.py72790%233–235, 362, 364–365, 386
   functional.py43588%46–49, 84
   longitudinal.py360100% 
   metrics.py23195%44
   qc.py170100% 
   query.py674237%103–107, 121–125, 127–128, 130–135, 137, 139, 153, 159–165, 198, 207, 209–216, 225, 257, 266–267
   session.py470100% 
rbc/cli
   __init__.py10100% 
   all.py49295%84, 109
   anatomical.py25292%47, 63
   base.py71987%56, 62, 123–125, 131–133, 156
   functional.py33293%67, 87
   longitudinal.py29293%48, 66
   main.py420100% 
   metrics.py42295%77, 95
   qc.py25292%45, 61
rbc/core
   __init__.py30100% 
   common.py261253%43–45, 62–70
   fileops.py27485%69–72
   fsl2itk.py420100% 
   nifti.py192597%236–237, 244–245, 524
   niwrap.py56198%58
rbc/core/anatomical
   __init__.py40100% 
   registration.py15473%59, 151, 166, 183
   segmentation.py24866%64, 74–76, 92, 114, 125, 141
rbc/core/functional
   __init__.py130100% 
   coregistration.py7271%44, 55
   despiking.py7357%32, 36–37
   distortion.py1304069%269–271, 321, 324, 332–335, 341–346, 349, 352–353, 356, 365–369, 375–376, 387–388, 391, 397, 443–444, 447, 455, 461, 470–471, 474, 484, 491
   erosion.py32196%50
   initialization.py9455%35, 42–43, 63
   masking.py342526%53, 55–56, 58–59, 62–65, 69, 91, 134, 183, 197, 208, 223, 233, 249, 258, 271, 285, 296, 306, 319, 328
   motion.py573735%62, 64–67, 69, 71, 73, 76–77, 83–84, 86–87, 95–97, 99, 102, 105, 107, 124–125, 135–138, 159, 169, 171–172, 175, 177–179, 181, 183
   nuisance.py816025%78, 80–85, 87, 89–90, 93, 96–98, 100–102, 104–110, 112–116, 118, 163, 165, 167, 170–171, 173–175, 178, 181–182, 185–187, 190–193, 197, 203–205, 207, 235, 243, 269, 278, 307, 316, 322
   regressors.py89693%163, 193, 325–328
   resampling.py544320%37–42, 74–76, 78, 80–81, 85–87, 90, 93, 105, 107–108, 112, 114, 150–152, 154–155, 159, 161–162, 167–169, 173–174, 177, 180, 187, 199, 201–202, 207, 209
   timing.py161131%46–47, 49–53, 58, 60, 66–67
rbc/core/longitudinal
   __init__.py10100% 
   transform.py46784%106–107, 165–168, 170
rbc/core/metrics
   __init__.py30100% 
   alff.py90198%265
   reho.py660100% 
   smoothing.py7357%36, 42–43
   standardization.py271159%64, 66–68, 70, 72–75, 77–78
   timeseries.py57198%120
rbc/core/qc
   __init__.py60100% 
   dvars.py260100% 
   motion.py310100% 
   registration.py410100% 
   xcp.py410100% 
rbc/orchestration
   __init__.py32293%90, 93
   all.py41978%131–132, 137, 145, 153–154, 171, 173–174
   anatomical.py372240%49–52, 57–58, 60–63, 85–87, 89–90, 94, 101, 104, 109–110, 116, 118
   functional.py411270%124–126, 128–129, 133, 139, 142, 147–148, 157, 159
   longitudinal.py56198%156
   metrics.py463034%32–35, 38–40, 67, 75–76, 100–102, 104–106, 110, 118–121, 123–124, 130–132, 137, 145, 152, 154
   qc.py340100% 
rbc/workflows
   __init__.py100100% 
   anatomical.py471959%90–93, 97–102, 107, 177–180, 182–184, 188
   functional.py1116640%115–116, 124, 129–130, 138, 209–210, 213–214, 217–218, 221, 224–227, 237–239, 249–250, 253, 256, 259–260, 268–269, 276–277, 284–285, 292–293, 296–298, 301–304, 316–317, 329, 331–334, 337–338, 347–348, 356, 363, 455, 462, 465, 470, 472–474, 476–477, 483–484, 490
   metrics.py411856%80, 83–84, 93–94, 99–102, 105–108, 112–115, 119
   qc.py553438%88–89, 92–93, 96, 99, 102–107, 110, 119–121, 125–128, 130–134, 136–137, 139–141, 144, 161, 167, 169
rbc_resources
   __init__.py380100% 
TOTAL327159381% 

Tests Skipped Failures Errors Time
729 0 💤 0 ❌ 0 🔥 10.132s ⏱️

@jpillai00 jpillai00 marked this pull request as ready for review April 8, 2026 23:44
@jpillai00 jpillai00 requested review from Copilot, kaitj and nx10 April 8, 2026 23:44
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extends the longitudinal functional workflow to re-run nuisance regression in longitudinal template space using the raw (non-bandpass-filtered) nuisance regressors produced during cross-sectional preprocessing, while also exporting additional regressor artifacts from the cross-sectional stage.

Changes:

  • Expand functional output structures to carry both raw and bandpass-filtered regressor .1D files, and add longitudinal-space regressed_bold/cleaned_bold outputs per regressor strategy.
  • Update longitudinal orchestration/BIDS resolve+export to accept a regressor strategy list and to resolve raw regressor files per strategy.
  • Add --regressor support to the longitudinal CLI and update unit tests accordingly.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/unit/orchestration/test_longitudinal.py Updates mocks and process_func calls to pass regressors; adjusts/removed tests impacted by new required inputs.
tests/unit/cli/test_longitudinal.py Adds regressor to the longitudinal CLI test namespace defaults.
tests/unit/bids/test_exports.py Updates expected derivative file counts due to new exported regressor artifact(s).
src/rbc/workflows/functional.py Adds raw+filtered regressor outputs to cross-sectional outputs; adds longitudinal-space regression outputs and implements re-regression in longitudinal space.
src/rbc/orchestration/longitudinal.py Threads regressor strategies through longitudinal functional processing; adds default regressors parameter.
src/rbc/cli/longitudinal.py Adds --regressor CLI option and passes it through to orchestration.
src/rbc/bids/longitudinal.py Resolves raw regressor .1D files per strategy and exports longitudinal-space regressed/cleaned BOLD outputs.
src/rbc/bids/functional.py Exports an additional bandpass-filtered regressor .1D artifact per regressor strategy.
Comments suppressed due to low confidence (1)

tests/unit/orchestration/test_longitudinal.py:346

  • test_missing_required_file_raises no longer covers the newly-required longitudinal inputs (bold_mask and the per-regressor regressor_files). Since resolve_longitudinal_func() now uses expect() for these, it would be good to extend the parametrization to assert a FileNotFoundError when the mask or a regressor .1D file is missing (per regressor strategy).
    @pytest.mark.parametrize(
        ("match_field", "match_kwargs"),
        [
            ("bold", {"suffix": "bold", "desc": "preproc"}),
            ("sbref", {"suffix": "sbref"}),
            (
                "bold_to_anat_xfm",
                {
                    "suffix": "xfm",
                    "desc": "linear",
                    "extension": ".txt",
                    "extra": {"from": "bold", "to": "T1w", "mode": "image"},
                },
            ),
        ],

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/rbc/orchestration/longitudinal.py Outdated
Comment thread src/rbc/bids/longitudinal.py
Copy link
Copy Markdown
Contributor

@kaitj kaitj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Largely looks good to me, a couple of minor changes with the type hints and casting. Also looks like it needs a rebase now.

One thing that comes to mind since the longitudinal currently relies on flags to denote the modality to run (--anatomical, --functional), should there be an early check to ensure the anatomical has been run? It should error out pretty early none the less since the data is relied upon on in the first step.

Comment thread src/rbc/cli/longitudinal.py Outdated
@jpillai00 jpillai00 force-pushed the add/long-func-outputs branch from ce86168 to 657623e Compare April 9, 2026 15:13
@jpillai00 jpillai00 linked an issue Apr 9, 2026 that may be closed by this pull request
@jpillai00 jpillai00 force-pushed the add/long-func-outputs branch from 657623e to d4fa651 Compare April 10, 2026 20:08
@nx10
Copy link
Copy Markdown
Contributor

nx10 commented Apr 13, 2026

Thanks @jpillai00 — the regressor_file vs bpf_regressor_file split and per-strategy regressed_bold/cleaned_bold dicts are the right shape; they just need to land on the new submodule layout. Closing as superseded by #301; Stage 5 rewrites this against workflows/longitudinal/functional.py. Port commits will carry Co-authored-by: Janhavi Pillai trailers.

Closed as part of #301 Stage 0.

@nx10 nx10 closed this Apr 13, 2026
nx10 added a commit that referenced this pull request Apr 16, 2026
regressor_file now carries the raw (unfiltered) regressor .1D files as
computed from native-space BOLD.  The new bpf_regressor_file field
holds the bandpass-filtered version that 3dTproject actually applied,
exported with desc-<strategy>Filtered for provenance.

This lets the longitudinal pipeline reuse the raw regressors without
recomputation, matching the cross-sectional principle: one regressor
computation per run, applied in each target space.

Port of the regressor-split idea from #282.

Co-authored-by: Janhavi Pillai <janhaviiyengar@gmail.com>
nx10 added a commit that referenced this pull request Apr 16, 2026
FunctionalLongOutputs gains regressed_bold and cleaned_bold dicts
keyed by regressor strategy.  longitudinal_process now accepts
regressor_files (raw .1D from cross-sectional) and re-runs
apply_regression + apply_regression_bandpass on the warped BOLD.
No regressor recomputation -- same matrix, different target space.

bold_mask is now mandatory (was Path | None).

BIDS export writes per-regressor desc-regressed and desc-preproc BOLD
with reg-<strategy> entity.  resolve_longitudinal_func resolves the
raw regressor files per strategy.

CLI gains --regressor with the same choices as cross-sectional
(36-parameter, aCompCor).

Port of the longitudinal regression logic from #282.

Co-authored-by: Janhavi Pillai <janhaviiyengar@gmail.com>
nx10 added a commit that referenced this pull request Apr 16, 2026
regressor_file now carries the raw (unfiltered) regressor .1D files as
computed from native-space BOLD.  The new bpf_regressor_file field
holds the bandpass-filtered version that 3dTproject actually applied,
exported with desc-<strategy>Filtered for provenance.

This lets the longitudinal pipeline reuse the raw regressors without
recomputation, matching the cross-sectional principle: one regressor
computation per run, applied in each target space.

Port of the regressor-split idea from #282.

Co-authored-by: Janhavi Pillai <janhavi.pillai@gmail.com>
nx10 added a commit that referenced this pull request Apr 16, 2026
FunctionalLongOutputs gains regressed_bold and cleaned_bold dicts
keyed by regressor strategy.  longitudinal_process now accepts
regressor_files (raw .1D from cross-sectional) and re-runs
apply_regression + apply_regression_bandpass on the warped BOLD.
No regressor recomputation -- same matrix, different target space.

bold_mask is now mandatory (was Path | None).

BIDS export writes per-regressor desc-regressed and desc-preproc BOLD
with reg-<strategy> entity.  resolve_longitudinal_func resolves the
raw regressor files per strategy.

CLI gains --regressor with the same choices as cross-sectional
(36-parameter, aCompCor).

Port of the longitudinal regression logic from #282.

Co-authored-by: Janhavi Pillai <janhavi.pillai@gmail.com>
nx10 added a commit that referenced this pull request Apr 16, 2026
* Split FunctionalOutputs.regressor_file into raw + bandpass-filtered

regressor_file now carries the raw (unfiltered) regressor .1D files as
computed from native-space BOLD.  The new bpf_regressor_file field
holds the bandpass-filtered version that 3dTproject actually applied,
exported with desc-<strategy>Filtered for provenance.

This lets the longitudinal pipeline reuse the raw regressors without
recomputation, matching the cross-sectional principle: one regressor
computation per run, applied in each target space.

Port of the regressor-split idea from #282.

Co-authored-by: Janhavi Pillai <janhavi.pillai@gmail.com>

* Add per-regressor regression to longitudinal functional workflow

FunctionalLongOutputs gains regressed_bold and cleaned_bold dicts
keyed by regressor strategy.  longitudinal_process now accepts
regressor_files (raw .1D from cross-sectional) and re-runs
apply_regression + apply_regression_bandpass on the warped BOLD.
No regressor recomputation -- same matrix, different target space.

bold_mask is now mandatory (was Path | None).

BIDS export writes per-regressor desc-regressed and desc-preproc BOLD
with reg-<strategy> entity.  resolve_longitudinal_func resolves the
raw regressor files per strategy.

CLI gains --regressor with the same choices as cross-sectional
(36-parameter, aCompCor).

Port of the longitudinal regression logic from #282.

Co-authored-by: Janhavi Pillai <janhavi.pillai@gmail.com>

* Add tests for longitudinal functional regression (Stage 5 of #301)

Unit tests: resolve_longitudinal_func with single/multiple regressors,
missing regressor raises, mandatory bold_mask; export file counts and
reg-<strategy> entity validation.

Tier-2 integration: apply_regression and apply_regression_bandpass on
warped BOLD with raw regressors produce non-degenerate outputs.

Tier-4 full_pipeline: longitudinal functional outputs exist, regressed
and cleaned BOLD have non-zero variance, bold mask is binary.

* Remove redundant regressor_set, replace fake tests with real ds000114 integration

- Drop regressor_set param from longitudinal_process; iterate
  regressor_files.keys() instead.  The resolve layer already filters to
  the requested strategies, so the dict keys ARE the set.  Eliminates
  the caller sync burden.

- Delete tests/full_pipeline/longitudinal/ -- the conftest hacked the
  cross-sectional anat brain as a "longitudinal template", which
  exercises the code path but not the actual transform chain.

- Rewrite tests/integration/longitudinal/test_regression_reuse.py as a
  proper end-to-end CLI test: rbc functional -> rbc longitudinal
  functional on ds000114 sub-01 ses-test via subprocess.  Asserts
  expected BIDS tree, non-degenerate variance, and binary mask.

- Add ds000114_func_derivatives and longitudinal_func_output fixtures
  to the integration conftest so the full cross-sectional -> longitudinal
  chain runs on real multi-session data with docker.

* Fix integration test fixture ordering to avoid bids2table discovery issue

ds000114_func_derivatives now depends on ds000114_anat_derivatives
directly instead of longitudinal_template_output.  This ensures
rbc functional runs before the template step writes ses-longitudinal
files into the derivatives dir, avoiding a potential bids2table
discovery issue where the ses-longitudinal anat files interfere with
the functional pipeline's anat resolution.

longitudinal_func_output depends on both ds000114_func_derivatives
and longitudinal_template_output to ensure both cross-sectional
functional outputs and the longitudinal template are present before
rbc longitudinal functional runs.

* Debug: log derivatives tree before rbc functional in integration fixture

* Debug: dump bids2table output for derivatives dir before rbc functional

* Debug: inline rbc functional call with verbose + tree dump on failure

* Fix integration fixture: --task filter drops anat rows

Filters.apply() applies the --task filter to ALL rows including anat.
Anat files have task=null, so --task fingerfootlips drops them,
causing resolve_functional to fail with FileNotFoundError on
desc-brain_T1w.

Drop --task from both rbc functional and rbc longitudinal functional
fixture calls.  ds000114 only has one task per session so the filter
isn't needed.  Remove debug logging from previous commits.

* Fix Filters.apply() task filter dropping anat rows

The --task filter applied pl.col("task") == value to all rows, but
anat/dwi files have task=null, so they were silently dropped.  This
caused rbc functional --task <label> to fail with FileNotFoundError
on desc-brain_T1w for any dataset.

Fix: use pl.col("task").is_null() | (pl.col("task") == value) so
rows without a task entity (anat, dwi, fmap) pass through.

Affects functional and all orchestration; metrics/qc pre-filter to
datatype=func so they were never hit.

Restore --task in integration fixtures now that the fix is in place.

* Fix expected filenames: reg entity precedes desc in BIDS builder output

---------

Co-authored-by: Janhavi Pillai <janhavi.pillai@gmail.com>
@kaitj kaitj deleted the add/long-func-outputs branch May 20, 2026 23:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update longitudinal functional workflow outputs

4 participants